- 因為程式跟文章都是當天寫(想),有錯字、語病跟問題請大家在留言給我。
- 範例會放在 Controller 101 維護。
在動手實作 Kubernetes 自定義控制器 Part3 文章中,了解如何實現自定義控制器的高可靠架構,而今天將延續之前位完成的部分,會簡單以 Docker 實作一個虛擬機驅動來提供給自定義控制器使用,控制器會依據自定義資源VirtualMachine
的內容,來協調完成預期結果的事情。如下架構圖所示。
由於為了方便大家在 Minikube 上執行這個自定義控制器,因此這邊實作了一個 VM Driver 的 Golang 介面,並以該介面實現一個 Docker Driver 來作為使用。這個 Docker Driver 會以 Docker 預設的系統環境變數來載入 Endpoint、Certs 等等 Docker client 需要的資訊,接著透過這些資訊建立一個 client 與 Docker API 溝通進行各種操作。這邊介面只簡單實現以下函式來完成範例:
type Interface interface {
CreateServer(*CreateRequest) (*CreateReponse, error)
DeleteServer(name string) error
IsServerExist(name string) (bool, error)
GetServerStatus(name string) (*GetStatusReponse, error)
}
當控制器收到 VirtualMachine 的實例建立時,控制器會在syncHandler()
函式依據接受到的資源物件資訊來呼叫 VM Driver 進行處理相關事情(如建立虛擬機環境、更新虛擬機使用率等等),當處理完成後,再依據回應的內容更新到 VirtualMachine 資源實例的.status
內容。而當控制器收到有個 VirtualMachine 實例被刪除時,就會呼叫 Informer 的DeleteFunc
來進行處理實際虛擬機移除的事情。
- 由於這只是為了說明如何開發控制器,因此該範例使用的 Docker Driver 在建立容器時,只會以 NGINX 映像檔為基礎來建立。
- 原本規劃 Fake Driver 與 KVM 來模擬,但因為時間關析,只能之後再補上。
本部分將修改controller.go
程式,以實現自定義資源 VirtualMachine 管理虛擬機的機制。
由於使用這個功能需要用到 Kubernetes 與 Go 語言,因此需要透過以下來完成條件:
minikube start --kubernetes-version=v1.15.4
。eval $(minikube docker-env)
來取的 Docker 參數,並遠端操作。前幾天我們在實作控制器時,有提到主要處理 API 資源實例的函式是syncHandler()
,因此大部分邏輯會在這邊實現。但由於該控制器需要透過一些方法管理實際的虛擬機,因此這邊以 VM Driver 實現 Docker Driver 方式進行模擬。這時要修改控制器結構與建構子如下。
type Controller struct {
...
vm driver.Interface // 管理實際虛擬機的驅動程式
}
func New(clientset cloudnative.Interface, informer cloudnativeinformer.SharedInformerFactory, vm driver.Interface) *Controller {
...
controller := &Controller{
...
vm: vm,
}
...
return controller
}
...
表示不更改內容。完整程式請參考 controller.go L44-L72
完成後,就可以透過上面物件,在這個控制器結構的函式操作虛擬機。接著在syncHandler()
實現協調循環的邏輯:
func (c *Controller) syncHandler(key string) error {
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))
return err
}
vm, err := c.lister.VirtualMachines(namespace).Get(name)
if err != nil {
if errors.IsNotFound(err) {
utilruntime.HandleError(fmt.Errorf("virtualmachine '%s' in work queue no longer exists", key))
return err
}
return err
}
switch vm.Status.Phase {
case v1alpha1.VirtualMachineNone:
if err := c.makeCreatingPhase(vm); err != nil {
return err
}
case v1alpha1.VirtualMachinePending, v1alpha1.VirtualMachineFailed:
if err := c.createServer(vm); err != nil {
return err
}
case v1alpha1.VirtualMachineActive:
if err := c.updateUsage(vm); err != nil {
return err
}
}
return nil
}
// 用於更新 VirtualMachine 資源狀態的通用函式
func (c *Controller) updateStatus(vm *v1alpha1.VirtualMachine, phase v1alpha1.VirtualMachinePhase, reason error) error {
vm.Status.Reason = ""
if reason != nil {
vm.Status.Reason = reason.Error()
}
vm.Status.Phase = phase
vm.Status.LastUpdateTime = metav1.NewTime(time.Now())
_, err := c.clientset.CloudnativeV1alpha1().VirtualMachines(vm.Namespace).Update(vm)
return err
}
// 用於將虛擬機狀態新增到 VirtualMachine 資源的通用函式
func (c *Controller) appendServerStatus(vm *v1alpha1.VirtualMachine) error {
status, err := c.vm.GetServerStatus(vm.Name)
if err != nil {
return err
}
vm.Status.Server.Usage.CPU = status.CPUPercentage
vm.Status.Server.Usage.Memory = status.MemoryPercentage
vm.Status.Server.State = status.State
return nil
}
當收到 Informer 的 Add/Update 事件時,會將資源實例的物件放到 Workqueue,然後控制器的 Workers 會呼叫processNextWorkItem()
函式來持續消化 Workqueue 中的物件,並在取出物件的 Key 後,將其丟到syncHandler()
函式處理。而syncHandler()
函式會透過 Lister 從本地快取中獲取資源實例的內容,這時我們就能透過內容的狀態來處理對應事情。以上面程式為例,我們分成以下幾個狀態來處理。
這邊使用不同狀態來處理不同過程,其目的是確保控制器不會因為實例的狀態更新,而一直觸發 Update 事件導致無限循環,因此以狀態來做收斂的點。
func (c *Controller) makeCreatingPhase(vm *v1alpha1.VirtualMachine) error {
vmCopy := vm.DeepCopy()
return c.updateStatus(vmCopy, v1alpha1.VirtualMachineCreating, nil)
}
.status
為 Active 狀態;若失敗的話,則標示為 Failed,狀態,並提供失敗原因的訊息。程式內容如下:func (c *Controller) createServer(vm *v1alpha1.VirtualMachine) error {
vmCopy := vm.DeepCopy()
ok, _ := c.vm.IsServerExist(vm.Name)
if !ok {
req := &driver.CreateRequest{
Name: vm.Name,
CPU: vm.Spec.Resource.Cpu().Value(),
Memory: vm.Spec.Resource.Memory().Value(),
}
resp, err := c.vm.CreateServer(req)
if err != nil {
if err := c.updateStatus(vmCopy, v1alpha1.VirtualMachineFailed, err); err != nil {
return err
}
return err
}
vmCopy.Status.Server.ID = resp.ID
if err := c.appendServerStatus(vmCopy); err != nil {
return err
}
if err := c.updateStatus(vmCopy, v1alpha1.VirtualMachineActive, nil); err != nil {
return err
}
}
return nil
}
func (c *Controller) updateUsage(vm *v1alpha1.VirtualMachine) error {
vmCopy := vm.DeepCopy()
t := subtractTime(vmCopy.Status.LastUpdateTime.Time)
if t.Seconds() > periodSec {
if err := c.appendServerStatus(vmCopy); err != nil {
return err
}
if err := c.updateStatus(vmCopy, v1alpha1.VirtualMachineActive, nil); err != nil {
return err
}
}
return nil
}
這邊
subtractTime()
用於避免控制器一直執行updateStatus()
,而導致無限循環。
而當 API 資源物件被刪除時,Informer 會呼叫DeleteFunc
的對應函式deleteObject()
來刪除虛擬機。程式內容如下:
func (c *Controller) deleteObject(obj interface{}) {
vm := obj.(*v1alpha1.VirtualMachine)
if err := c.vm.DeleteServer(vm.Name); err != nil {
klog.Errorf("Failed to delete the '%s' server: %v", vm.Name, err)
}
}
當 Controller 與 VM Driver 程式都完成後,就可以修改主程式來反映功能改變:
var (
...
driverName string
)
func parseFlags() {
...
flag.StringVarP(&driverName, "vm-driver", "", "", "Driver is one of: [fake docker].")
...
}
func main() {
...
var vmDriver driver.Interface
switch driverName {
case "docker":
docker, err := driver.NewDockerDriver()
if err != nil {
klog.Fatalf("Error to create docker driver: %s", err.Error())
}
vmDriver = docker
default:
klog.Fatalf("The driver '%s' is not supported.", driverName)
}
...
controller := controller.New(clientset, informer, vmDriver)
...
}
...
表示不更改內容。完整程式請參考 main.co
當上述功能實現後,且已有新增完 VirtualMachine CRD 的 Kubernetes 環境時,就可以執行以下指令來啟動控制器:
$ eval $(minikube docker-env)
$ go run cmd/main.go --kubeconfig=$HOME/.kube/config \
-v=3 --logtostderr \
--leader-elect=false \
--vm-driver=docker
...
I1015 16:02:57.180484 62884 controller.go:77] Starting the controller
I1015 16:02:57.180665 62884 controller.go:78] Waiting for the informer caches to sync
I1015 16:02:57.285693 62884 controller.go:86] Started workers
接著開啟另一個 Terminal 來建立 VirtualMachine 資源實例。當建立時,會發現控制器更新了 test-vm 資源實例,這時可以利用 kubectl 查看狀態:
$ cat <<EOF | kubectl apply -f -
apiVersion: cloudnative.tw/v1alpha1
kind: VirtualMachine
metadata:
name: test-vm
spec:
resource:
cpu: 2
memory: 4G
EOF
virtualmachine.cloudnative.tw/test-vm created
$ kubectl get vms
NAME STATUS CPU MEMORY AGE
test-vm Active 0 0.10977787071142493 44s
由於本範例使用 Docker 作為虛擬機驅動程式,因此該資源實際上是建立一個容器。我們可以利用 docker 指令來查看:
$ docker ps --filter "name=test-vm"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b0d7f2be48e5 nginx:1.17.4 "nginx -g 'daemon of…" 8 seconds ago Up 6 seconds 80/tcp test-vm
$ docker inspect test-vm -f "{{.HostConfig.Memory}}"
4000000000
$ docker inspect test-vm -f "{{.HostConfig.NanoCpus}}"
2
接著我們來增加這個 NIGNX 的工作負載,以查看 CPU 變化:
$ IP=$(docker inspect test-vm -f "{{.NetworkSettings.IPAddress}}")
$ docker run --rm -it busybox /bin/sh -c "while :; do wget -O- ${IP}; done"
開啟新 Terminal 以 kubectl 指令來查看:
$ kubectl get vms -w
NAME STATUS CPU MEMORY AGE
test-vm Active 0 0.11279374628042013 5m30s
test-vm Active 15.637706179775282 0.11279374628042013 5m43s
test-vm Active 15.688157325581395 0.11299480465168647 6m13s
test-vm Active 15.55665426966292 0.11279374628042013 6m43s
由於控制器設計關析,CPU 與 Memory 只會每 30s 同步一次。
今天將控制器管理 VirtualMachine 資源實例的邏輯完成。一但完成,就能利用 Kubernetes-like API 來管理虛擬機的生命週期,或取得虛擬機狀態等等事情。然而今天實作部分,事實上還有一些問題存在,比如說我們先把自定義控制器暫時關閉,然後執行kubectl delete vm test-vm
指令來將該資源實例從 Kubernetes 中刪除,這時查看虛擬機列表(因為 VM Driver 為 Docker,因此對應查看為 docker ps
)時,就會發現被管理的虛擬機依然存在,並且當重新啟動控制器時,也會因為該資源實例已經被刪除,因此無法讓控制器來協助刪除,這樣就會形成殭屍虛擬機問題。如下圖所示。
從 Controller 程式碼中也可以從
deleteObject()
看出問題,因為這邊若發生刪除錯誤,就會造成外部資源變成殭屍(或孤兒)。
那麼當遇到這個問題時,該怎麼解決呢?明天我們將針對這部份來實作,以確保 API 資源實例一定要先刪除所管理的虛擬機後,才能從 Kubernetes 叢集中刪除。